package com.gitee.cirnochat.login.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.gitee.cirnochat.common.config.JwtProperties;
import com.gitee.cirnochat.common.event.UserRegisterEvent;
import com.gitee.cirnochat.common.exception.BusinessException;
import com.gitee.cirnochat.common.model.enums.ErrorCode;
import com.gitee.cirnochat.common.utils.JwtTool;
import com.gitee.cirnochat.login.annotation.TrackLogin;
import com.gitee.cirnochat.login.annotation.TrackLogout;
import com.gitee.cirnochat.login.model.dto.LoginQrCodeStatus;
import com.gitee.cirnochat.login.model.enums.QrCodeStatus;
import com.gitee.cirnochat.login.model.enums.QrCodeType;
import com.gitee.cirnochat.login.model.vo.req.UserEmailCodeLoginRequest;
import com.gitee.cirnochat.login.model.vo.req.UserEmailPasswordLoginRequest;
import com.gitee.cirnochat.login.service.UserLoginService;
import com.gitee.cirnochat.login.service.function.UserLoginStartCallBack;
import com.gitee.cirnochat.login.service.function.UserLoginSuccessCallBack;
import com.gitee.cirnochat.login.service.function.UserLogoutCallBack;
import com.gitee.cirnochat.login.service.function.UserRefreshTokenCallBack;
import com.gitee.cirnochat.user.model.entity.User;
import com.gitee.cirnochat.user.model.vo.resp.UserVo;
import com.gitee.cirnochat.user.service.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

import static com.gitee.cirnochat.common.constant.QRCodeConstant.LOGIN_SCANNED_QR_CODE_EXPIRE_TIME;
import static com.gitee.cirnochat.common.constant.RedisKey.*;

@Service
@RequiredArgsConstructor
@Slf4j
public class UserLoginServiceImpl implements UserLoginService {
    private final UserService userService;
    private final StringRedisTemplate redisTemplate;
    private final JwtTool jwtTool;
    private final JwtProperties jwtProperties;
    private final ObjectMapper objectMapper;
    private final PasswordEncoder passwordEncoder;
    private final ApplicationEventPublisher eventPublisher;

    @TrackLogin
    @Override
    public UserVo emailCodeLogin(UserEmailCodeLoginRequest request,
                                 String deviceId,
                                 UserLoginStartCallBack onLoginStart,
                                 UserLoginSuccessCallBack onLoginSuccess) {
        // 查询用户
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getEmail, request.getEmail());
        User user = userService.getOne(queryWrapper);
        long userId = user == null ? IdWorker.getId() : user.getId();
        String uuid = user == null ? UUID.randomUUID().toString().replaceAll("-", "").substring(0, 20) : user.getUuid();
        // 登录开始
        onLoginStart.onUserLoginStart(userId);
        // 验证码校验
        String verifyCode = redisTemplate.opsForValue().get(EMAIL_LOGIN_CAPTCHA_CACHE_KEY + request.getEmail());
        if (verifyCode == null) {
            throw new BusinessException(ErrorCode.OPERATION_ERROR, "验证码已失效");
        }
        if (!request.getCode().equals(verifyCode)) {
            throw new BusinessException(ErrorCode.OPERATION_ERROR, "验证码错误");
        }
        // 用户不存在则创建新用户
        if (user == null) {
            user = new User();
            user.setId(userId);
            user.setUuid(uuid);
            BeanUtils.copyProperties(request, user);
            userService.save(user);
            // 发布注册事件
            eventPublisher.publishEvent(new UserRegisterEvent(this, userId));
        }
        // 清除缓存中的验证码
        redisTemplate.delete(EMAIL_LOGIN_CAPTCHA_CACHE_KEY + request.getEmail());
        // 生成 JWT Token
        String ak = generateAccessToken(userId, deviceId);
        String rk = generateRefreshToken(userId, deviceId);
        onLoginSuccess.onUserLoginSuccess(ak,rk,jwtProperties.getRefreshTokenTTL().getSeconds());
        // 返回用户信息
        return new UserVo(userService.getById(userId));
    }
    @TrackLogin
    @Override
    public UserVo emailPasswordLogin(UserEmailPasswordLoginRequest request, String deviceId, UserLoginStartCallBack onLoginStart, UserLoginSuccessCallBack onLoginSuccess) {
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getEmail, request.getEmail());
        User user = userService.getOne(queryWrapper);
        if (user == null) {
            throw new BusinessException(ErrorCode.OPERATION_ERROR, "用户不存在");
        }
        // 登录开始
        onLoginStart.onUserLoginStart(user.getId());
        if (user.getPassword() == null || user.getPassword().isEmpty()) {
            throw new BusinessException(ErrorCode.OPERATION_ERROR, "用户未设置密码");
        }

        if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
            throw new BusinessException(ErrorCode.OPERATION_ERROR, "密码错误");
        }
        // 生成 JWT Token
        String ak = generateAccessToken(user.getId(), deviceId);
        String rk = generateRefreshToken(user.getId(), deviceId);
        onLoginSuccess.onUserLoginSuccess(ak,rk, jwtProperties.getRefreshTokenTTL().getSeconds());
        // 返回用户信息
        return new UserVo(user);
    }

    @Override
    public void scanQrCodeLogin(String qrCodeId, Long userId) {
        Set<String> keys = redisTemplate.keys(QR_CODE_ID_KEY + String.format("*#%d#%s", QrCodeType.LOGIN.getValue(), qrCodeId));
        if (keys != null && !keys.isEmpty()) {
            String realKey = keys.iterator().next();
            String s = redisTemplate.opsForValue().get(realKey);
            LoginQrCodeStatus loginQrCodeStatus;
            try {
                loginQrCodeStatus = objectMapper.readValue(s, LoginQrCodeStatus.class);
            } catch (JsonProcessingException e) {
                throw new RuntimeException(e);
            }
            if (loginQrCodeStatus.getStatus() == QrCodeStatus.SCANNED) {
                throw new BusinessException(ErrorCode.OPERATION_ERROR, "二维码已被扫描");
            }
            loginQrCodeStatus.setUser(userId);
            loginQrCodeStatus.setStatus(QrCodeStatus.SCANNED);
            loginQrCodeStatus.setExpireTime(loginQrCodeStatus.getCreateTime() + LOGIN_SCANNED_QR_CODE_EXPIRE_TIME.toMillis());
            try {
                redisTemplate.opsForValue().set(realKey,objectMapper.writeValueAsString(loginQrCodeStatus),LOGIN_SCANNED_QR_CODE_EXPIRE_TIME);
            } catch (JsonProcessingException e) {
                throw new RuntimeException(e);
            }
        } else {
            throw new BusinessException(ErrorCode.OPERATION_ERROR, "二维码已失效");
        }
    }
    @TrackLogin
    @Override
    public UserVo getLoginQrCodeResult(String qrCodeId, String deviceId , UserLoginStartCallBack onLoginStart, UserLoginSuccessCallBack onLoginSuccess) {
        String scanKey = QR_CODE_ID_KEY + String.format("%s#%d#%s", deviceId,QrCodeType.LOGIN.getValue(), qrCodeId);
        String s = redisTemplate.opsForValue().get(scanKey);
        if (s == null) {
            throw new BusinessException(ErrorCode.OPERATION_ERROR, "二维码已失效");
        }
        LoginQrCodeStatus loginQrCodeStatus;
        try {
            loginQrCodeStatus = objectMapper.readValue(s, LoginQrCodeStatus.class);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
        Long userId = loginQrCodeStatus.getUser();
        // 生成 JWT Token
        String ak = generateAccessToken(userId,deviceId);
        String rk = generateRefreshToken(userId, deviceId);
        onLoginSuccess.onUserLoginSuccess(ak,rk, jwtProperties.getRefreshTokenTTL().getSeconds());
        // 清除二维码状态
        redisTemplate.delete(QR_CODE_ID_KEY + String.format("%s#%d#%s", deviceId, QrCodeType.LOGIN.getValue(), qrCodeId));
        return new UserVo(userService.getById(userId));
    }


    @Override
    public void refreshToken(String refreshToken, Long userId , String deviceId ,  UserRefreshTokenCallBack onRefreshToken) {
        if (refreshToken == null || refreshToken.isEmpty()) {
            throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR, "请重新登录");
        }
        // 检查是否还在 redis 中（服务端登出后可撤销）
        String redisToken = redisTemplate.opsForValue().get(REFRESH_TOKEN_KEY + String.format("%s#%s", deviceId, userId));
        if (!refreshToken.equals(redisToken)) {
            throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR, "请重新登录");
        }
        // 生成新的 accessToken
        String newAccessToken = jwtTool.createAccessToken(userId, jwtProperties.getAccessTokenTTL());
        redisTemplate.opsForValue().set(ACCESS_TOKEN_KEY + String.format("%s#%s", deviceId, userId), String.valueOf(userId),
                jwtProperties.getAccessTokenTTL().toMillis(), TimeUnit.MILLISECONDS);
        onRefreshToken.onRefreshToken(newAccessToken);
    }

    /**
     * 生成 accessToken
     */
    private String generateAccessToken(Long userId , String deviceId) {
        // 将 accessToken 放到响应头
        String accessToken = jwtTool.createAccessToken(userId, jwtProperties.getAccessTokenTTL());
        // 缓存 accessToken
        redisTemplate.opsForValue().set(ACCESS_TOKEN_KEY + String.format("%s#%s", deviceId, userId), String.valueOf(userId),
                jwtProperties.getAccessTokenTTL().toMillis(), TimeUnit.MILLISECONDS);
        return accessToken;
    }

    /**
     * 生成 refreshToken
     */
    private String generateRefreshToken(Long userId , String deviceId) {
        // 生成 accessToken 和 refreshToken
        String refreshToken = jwtTool.createRefreshToken(userId, jwtProperties.getRefreshTokenTTL());
        // 缓存 refreshToken
        redisTemplate.opsForValue().set(REFRESH_TOKEN_KEY + String.format("%s#%s", deviceId, userId), refreshToken,
                jwtProperties.getRefreshTokenTTL().toMillis(), TimeUnit.MILLISECONDS);
         return refreshToken;
    }

    @Override
    @TrackLogout
    public void logout(Long userId , String deviceId,UserLogoutCallBack onLogout) {
        // 删除 accessToken redis key
        redisTemplate.delete(ACCESS_TOKEN_KEY + String.format("%s#%s", deviceId, userId));
        // 删除 refreshToken redis key
        redisTemplate.delete(REFRESH_TOKEN_KEY + String.format("%s#%s", deviceId, userId));
    }
}
